/********************************************************************************************************
 * @file MeshUtils.java
 *
 * @brief for TLSR chips
 *
 * @author telink
 * @date Sep. 30, 2017
 *
 * @par Copyright (c) 2017, Telink Semiconductor (Shanghai) Co., Ltd. ("TELINK")
 *
 *          Licensed under the Apache License, Version 2.0 (the "License");
 *          you may not use this file except in compliance with the License.
 *          You may obtain a copy of the License at
 *
 *              http://www.apache.org/licenses/LICENSE-2.0
 *
 *          Unless required by applicable law or agreed to in writing, software
 *          distributed under the License is distributed on an "AS IS" BASIS,
 *          WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *          See the License for the specific language governing permissions and
 *          limitations under the License.
 *******************************************************************************************************/
package com.telink.ble.mesh.core;

import android.bluetooth.BluetoothGatt;
import android.os.ParcelUuid;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;

import com.telink.ble.mesh.core.ble.MeshScanRecord;
import com.telink.ble.mesh.core.ble.UUIDInfo;
import com.telink.ble.mesh.core.message.MeshMessage;
import com.telink.ble.mesh.core.message.StatusMessage;
import com.telink.ble.mesh.core.message.aggregator.AggregatorItem;
import com.telink.ble.mesh.core.message.aggregator.OpcodeAggregatorStatusMessage;
import com.telink.ble.mesh.core.networking.AccessLayerPDU;
import com.telink.ble.mesh.util.Arrays;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.zip.CRC32;

public final class MeshUtils {

    //    public static final int DEVICE_ADDRESS_MAX = 0xFFFE; // 0x00FF
    // The maximum value for a unicast address
    public static final int UNICAST_ADDRESS_MAX = 0x7FFF;

    // The minimum value for a unicast address is set to
    public static final int UNICAST_ADDRESS_MIN = 0x0001;

    public static final int ADDRESS_BROADCAST = 0xFFFF;

    public static final int ADDRESS_ALL_PROXY = 0xFFFC;



    // 1970 -- 2000 offset second
    public static final long TAI_OFFSET_SECOND = 946684800;

    // This constant represents the maximum value that can be stored in an unsigned integer.
    public static final long UNSIGNED_INTEGER_MAX = 0xFFFFFFFFL;

    /**
     * sign used as message source address or dest address
     * if is valued, the message should be recognized as local message, and should not send out
     */
    public static final int LOCAL_MESSAGE_ADDRESS = 0;

    // ivIndex missing, used when import from JSON
    public static final long IV_MISSING = 0xFFFFFFFFL;

    // Object Transfer Service UUID
    public static final ParcelUuid OTS_UUID = new ParcelUuid(UUIDInfo.SERVICE_OTS);

    public static final ParcelUuid SOL_UUID = new ParcelUuid(UUIDInfo.SERVICE_MESH_PROXY_SOLICITATION);

    /**
     * used in {@link #generateChars(int)}
     */
    public static final String CHARS = "123456789aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ+-*/<>/?!@#$%^&;'[]{}|,.";

    private static final SimpleDateFormat COMPLETE_DATE_FORMAT = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss", Locale.getDefault());

    // provides a cryptographically strong random number generator
    private static SecureRandom rng;

    private MeshUtils() {
    }

    /**
     * Generates a random byte array of the specified length.
     *
     * @param length The length of the byte array to generate.
     * @return A randomly generated byte array.
     */
    public static byte[] generateRandom(int length) {

        byte[] data = new byte[length];

        synchronized (MeshUtils.class) {
            if (rng == null) {
                rng = new SecureRandom();
            }
        }

        rng.nextBytes(data);

        return data;
    }

    /**
     * This code is a method that returns the current TAI (International Atomic Time) time in seconds.
     */
    public static long getTaiTime() {
        return Calendar.getInstance().getTimeInMillis() / 1000 - TAI_OFFSET_SECOND;
    }

    /**
     * Mesh V1.0 3.7.3.1 Operation codes
     * Opcode Format
     * Notes
     * 0xxxxxxx (excluding 01111111)
     * 1-octet Opcodes
     * 01111111
     * Reserved for Future Use
     * 10xxxxxx xxxxxxxx
     * 2-octet Opcodes
     * 11xxxxxx zzzzzzzz
     * 3-octet Opcodes
     */

    /*public static OpcodeType getOpType(byte opFst) {
//        final int opVal = getValue();
        return (opFst & bit(7)) != 0
                ?
                ((opFst & bit(6)) != 0 ? OpcodeType.VENDOR : OpcodeType.SIG_2)
                :
                OpcodeType.SIG_1;
    }*/
    public static int bit(int n) {
        return 1 << n;
    }

    /**
     * This method generates a byte array of random characters.
     *
     * @param length the length of the byte array to be generated
     * @return a byte array containing random characters
     */
    public static byte[] generateChars(int length) {

        int charLen = CHARS.length() - 1;
        int charAt;

        byte[] data = new byte[length];

        for (int i = 0; i < length; i++) {
            charAt = (int) Math.round(Math.random() * charLen);
            data[i] = (byte) CHARS.charAt(charAt);
        }

        return data;
    }


    /**
     * convert byte buffer to integer
     *
     * @param buffer target buffer
     * @param order  {@link ByteOrder#BIG_ENDIAN} and {@link ByteOrder#LITTLE_ENDIAN}
     * @return int value, max 32-bit length
     */
    public static int bytes2Integer(byte[] buffer, ByteOrder order) {
        int re = 0;
        int valLen = Math.min(buffer.length, 4);
        for (int i = 0; i < valLen; i++) {
            if (order == ByteOrder.LITTLE_ENDIAN) {
                re |= (buffer[i] & 0xFF) << (8 * i);
            } else if (order == ByteOrder.BIG_ENDIAN) {
                re |= (buffer[i] & 0xFF) << (8 * (valLen - i - 1));
            }
        }
        return re;
    }

    /**
     * @param buffer target buffer
     * @param offset buffer start position
     * @param size   selected bytes length
     * @param order  ByteOrder
     * @return int value
     */
    public static int bytes2Integer(byte[] buffer, int offset, int size, ByteOrder order) {
        int re = 0;
        int valLen = Math.min(4, size);
        for (int i = 0; i < valLen; i++) {
            if (order == ByteOrder.LITTLE_ENDIAN) {
                re |= (buffer[i + offset] & 0xFF) << (8 * i);
            } else if (order == ByteOrder.BIG_ENDIAN) {
                re |= (buffer[i + offset] & 0xFF) << (8 * (valLen - i - 1));
            }
        }
        return re;
    }

    /**
     * Converts a portion of a byte array to a long value.
     *
     * @param buffer the byte array containing the data
     * @param offset the starting index of the portion in the byte array
     * @param size   the size of the portion in bytes
     * @param order  the byte order of the data (either ByteOrder.LITTLE_ENDIAN or ByteOrder.BIG_ENDIAN)
     * @return the long value converted from the byte array portion
     */
    public static long bytes2Long(byte[] buffer, int offset, int size, ByteOrder order) {
        long re = 0;
        int valLen = Math.min(8, size);
        for (int i = 0; i < valLen; i++) {
            if (order == ByteOrder.LITTLE_ENDIAN) {
                re |= (buffer[i + offset] & 0xFF) << (8 * i);
            } else if (order == ByteOrder.BIG_ENDIAN) {
                re |= (buffer[i + offset] & 0xFF) << (8 * (valLen - i - 1));
            }
        }
        return re;
    }

    /**
     * Converts an integer value to a byte array of specified size and byte order.
     *
     * @param i     the integer value to convert
     * @param size  the size of the resulting byte array (limited to maximum of 4 bytes)
     * @param order the byte order to use for the conversion (either ByteOrder.LITTLE_ENDIAN or ByteOrder.BIG_ENDIAN)
     * @return the byte array representation of the integer value
     */
    public static byte[] integer2Bytes(int i, int size, ByteOrder order) {
        if (size > 4) size = 4;
        byte[] re = new byte[size];
        for (int j = 0; j < size; j++) {
            if (order == ByteOrder.LITTLE_ENDIAN) {
                re[j] = (byte) (i >> (8 * j));
            } else {
                re[size - j - 1] = (byte) (i >> (8 * j));
            }
        }
        return re;
    }

    /**
     * converts a hexadecimal string to an integer using little-endian byte order.
     *
     * @param hex input
     * @return int value
     * little endian
     */
    public static int hexToIntL(String hex) {
        byte[] buf = Arrays.hexToBytes(hex);
        return bytes2Integer(buf, ByteOrder.LITTLE_ENDIAN);
    }

    /**
     * converts a hexadecimal string to an integer using big-endian byte order.
     *
     * @param hex input
     * @return int value
     * big endian
     */
    public static int hexToIntB(String hex) {
        return Integer.valueOf(hex, 16);
    }

    /**
     * big endian
     */
    public static String intToHex1(int hex) {
        return String.format("%02X", hex);
    }

    /**
     * big endian
     */
    public static String intToHex2(int hex) {
        return String.format(FORMAT_2_BYTES, hex);
    }


    /**
     * big endian
     */
    public static String intToHex3(int hex) {
        return String.format(FORMAT_3_BYTES, hex);
    }


    /**
     * big endian
     */
    public static String intToHex4(int hex) {
        return String.format(FORMAT_4_BYTES, hex);
    }

    /**
     * Converts a sequence number to a byte array buffer.
     *
     * @param sequenceNumber the sequence number to be converted
     * @return the byte array buffer representing the sequence number
     */
    public static byte[] sequenceNumber2Buffer(int sequenceNumber) {
        return integer2Bytes(sequenceNumber, 3, ByteOrder.BIG_ENDIAN);
    }

    /**
     * generate Aid (Application Identifier)
     *
     * @param key key
     * @return result
     */
    public static byte generateAid(byte[] key) {
        return Encipher.k4(key);
    }

    /**
     * This method returns the size of the Message Integrity Code (MIC) based on the given byte value.
     *
     * @return mic size
     */
    public static int getMicSize(byte szmic) {
        return szmic == 0 ? 4 : 8;
    }

    /**
     * Checks if the given address is a valid unicast address.
     *
     * @param address the address to be checked
     * @return true if the address is valid, false otherwise
     */
    public static boolean validUnicastAddress(int address) {
        return (address & 0xFFFF) <= UNICAST_ADDRESS_MAX && (address & 0xFFFF) >= UNICAST_ADDRESS_MIN;
    }

    /**
     * Checks whether the given address is a valid group address.
     * A group address is considered valid if the lower 16 bits of the address are between 0xC000 and 0xFEFF (inclusive).
     *
     * @param address the address to be checked
     * @return true if the address is a valid group address, false otherwise
     */
    public static boolean validGroupAddress(int address) {
        return (address & 0xFFFF) < 0xFF00 && (address & 0xFFFF) >= 0xC000;
    }

    /**
     * This method calculates the logarithm base 2 of a given number.
     *
     * @param i input
     * @return result
     */
    public static double mathLog2(int i) {
        return Math.log(i) / Math.log(2);
    }

    /**
     * This method compares two integers in an unsigned manner.
     *
     * @param a unsigned number
     * @param b unsigned number
     * @return result
     */
    public static long unsignedIntegerCompare(int a, int b) {
        return (a & UNSIGNED_INTEGER_MAX) - (b & UNSIGNED_INTEGER_MAX);
    }

    private static final String FORMAT_1_BYTES = "%02X";
    private static final String FORMAT_2_BYTES = "%04X";
    private static final String FORMAT_3_BYTES = "%06X";
    private static final String FORMAT_4_BYTES = "%08X";

    /**
     * Converts an integer value to its hexadecimal representation.
     *
     * @param value the integer value to be converted
     * @return the hexadecimal representation of the given value
     */
    public static String intToHex(int value) {
        if (value <= -1) {
            return String.format(FORMAT_4_BYTES, value);
        } else if (value <= 0xFF) {
            return String.format(FORMAT_1_BYTES, value);
        } else if (value <= 0xFFFF) {
            return String.format(FORMAT_2_BYTES, value);
        } else {
            return String.format(FORMAT_3_BYTES, value);
        }
    }

    /**
     * Converts an integer value to a hexadecimal string representation.
     * The length parameter specifies the number of bytes the value should be formatted to.
     * Returns the hexadecimal string representation of the value.
     */
    public static String intToHex(int value, int length) {
        if (length == 1) {
            return String.format(FORMAT_1_BYTES, value);
        } else if (length == 2) {
            return String.format(FORMAT_2_BYTES, value);
        } else if (length == 3) {
            return String.format(FORMAT_3_BYTES, value);
        } else {
            return String.format(FORMAT_4_BYTES, value);
        }
    }

    /**
     * iterates through each element in the list and checks if the current element is equal to the target string, ignoring case sensitivity.
     *
     * @param list   string list
     * @param target target string
     * @return whether list contains the target
     */
    public static boolean hexListContains(List<String> list, String target) {
        for (String s : list) {
            if (s.equalsIgnoreCase(target)) {
                return true;
            }
        }
        return false;
    }

    /**
     * iterates through each element in the list and checks if the current element is equal to the target integer, ignoring case sensitivity.
     *
     * @param list   string list
     * @param target target int
     * @return whether list contains the target
     */
    public static boolean hexListContains(List<String> list, int target) {
        int i;
        for (String s : list) {
            i = hexToIntB(s);
            if (i == target) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param unprovisioned true: get provision service data, false: get proxy service data
     */
    public static byte[] getMeshServiceData(byte[] scanRecord, boolean unprovisioned) {
        MeshScanRecord meshScanRecord = MeshScanRecord.parseFromBytes(scanRecord);
        return meshScanRecord.getServiceData(ParcelUuid.fromString((unprovisioned ? UUIDInfo.SERVICE_PROVISION : UUIDInfo.SERVICE_PROXY).toString()));
    }

    /**
     * get missing bit position
     */
    public static List<Integer> parseMissingBitField(@NonNull byte[] params, int offset) {
        List<Integer> missingChunks = new ArrayList<>();
        final int BYTE_LEN = 8;
        byte val;
        for (int basePosition = 0; offset < params.length; offset++, basePosition += BYTE_LEN) {
            val = params[offset];
            for (int i = 0; i < BYTE_LEN; i++) {
                boolean missing = ((val >> i) & 0b01) == 1;
                if (missing) {
                    missingChunks.add(basePosition + i);
                }
            }
        }
        return missingChunks;
    }


    /**
     * @return is certificate based supported
     */
    public static boolean isCertSupported(int oobInfo) {
        return (oobInfo & MeshUtils.bit(7)) != 0;
    }

    /**
     * @return is provisioning record supported
     */
    public static boolean isPvRecordSupported(int oobInfo) {
        return (oobInfo & MeshUtils.bit(8)) != 0;
    }

    /**
     * Converts a string representation of a UUID to a byte array.
     *
     * @param uuid the string representation of the UUID
     * @return the byte array representation of the UUID
     */
    public static byte[] uuidToByteArray(String uuid) {
        return uuidToByteArray(UUID.fromString(uuid));
    }

    /**
     * Converts a UUID to byte array.
     *
     * @param uuid UUID
     * @return the byte array representation of the UUID
     */
    public static byte[] uuidToByteArray(UUID uuid) {
        ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
        bb.putLong(uuid.getMostSignificantBits());
        bb.putLong(uuid.getLeastSignificantBits());
        return bb.array();
    }

    /**
     * This method converts a byte array to a UUID string.
     * It uses ByteBuffer to wrap the byte array and retrieve the long values for high and low parts of the UUID.
     * It then creates a new UUID using the retrieved values and returns its string representation.
     *
     * @param bytes the byte array to be converted to a UUID
     * @return the UUID string representation of the byte array
     */
    public static String byteArrayToUuid(byte[] bytes) {
        ByteBuffer bb = ByteBuffer.wrap(bytes);
        long high = bb.getLong();
        long low = bb.getLong();
        UUID uuid = new UUID(high, low);
        return uuid.toString();
    }


    /**
     * aggregates multiple mesh messages into a single byte array.
     *
     * @param elementAddress specifies the address of the element to which the messages belong.
     * @param meshMessages   list of MeshMessage objects containing the messages to be aggregated.
     * @return a byte array containing the aggregated messages.
     */
    public static byte[] aggregateMessages(int elementAddress, List<MeshMessage> meshMessages) {

        byte[] result = MeshUtils.integer2Bytes(elementAddress, 2, ByteOrder.LITTLE_ENDIAN);
        int len;
        boolean isLong;
        int bufLen;
        byte[] accessPdu;
        for (MeshMessage msg : meshMessages) {
            accessPdu = new AccessLayerPDU(msg.getOpcode(), msg.getParams()).toByteArray();
            len = accessPdu.length;
            isLong = len > 127;
            bufLen = (isLong ? 2 : 1) + len + result.length;

            ByteBuffer buffer = ByteBuffer.allocate(bufLen).order(ByteOrder.LITTLE_ENDIAN)
                    .put(result);

            len <<= 1 | (isLong ? 1 : 0);
            if (isLong) {
                buffer.putShort((short) len);
            } else {
                buffer.put((byte) len);
            }
            buffer.put(accessPdu);
            result = buffer.array();
        }
        return result;
    }

    /**
     * This method parses the OpcodeAggregatorStatusMessage and returns a list of StatusMessage objects.
     *
     * @param opAggStsMsg The OpcodeAggregatorStatusMessage to be parsed.
     * @return A list of StatusMessage objects generated from the OpcodeAggregatorStatusMessage.
     * Returns null if the statusItems list is empty or null.
     */
    public static List<StatusMessage> parseOpcodeAggregatorStatus(OpcodeAggregatorStatusMessage opAggStsMsg) {
        List<AggregatorItem> items = opAggStsMsg.statusItems;
        if (items == null || items.size() == 0) return null;
        List<StatusMessage> msgList = new ArrayList<>();
        StatusMessage msg;
        for (AggregatorItem item : items) {
            msg = StatusMessage.createByAccessMessage(item.opcode, item.parameters);
            msgList.add(msg);
        }
        return msgList;
    }

    /**
     * @param start
     * @param len
     * @return ByteOrder.LITTLE_ENDIAN
     */
    public static byte[] getUnicastRange(int start, @IntRange(from = 1, to = 0xFF) int len) {
        if (len == 1) {
            return integer2Bytes(start << 1, 2, ByteOrder.LITTLE_ENDIAN);
        } else {
            short sVal = (short) ((start << 1) | 0x01);
            return ByteBuffer.allocate(3).order(ByteOrder.LITTLE_ENDIAN)
                    .putShort(sVal)
                    .put((byte) len).array();
        }
    }

    /**
     * get the description of a GATT connection state based on the given connection state value.
     *
     * @param connectionState state int value
     * @return desc
     */
    public static String getGattConnectionDesc(int connectionState) {
        switch (connectionState) {
            case BluetoothGatt.STATE_DISCONNECTED:
                return "disconnected";

            case BluetoothGatt.STATE_CONNECTING:
                return "connecting...";

            case BluetoothGatt.STATE_CONNECTED:
                return "connected";

            case BluetoothGatt.STATE_DISCONNECTING:
                return "disconnecting...";

            default:
                return "unknown";
        }
    }

    /**
     * This method calculates the CRC32 checksum for a given byte array.
     *
     * @param data The byte array for which the CRC32 checksum needs to be calculated.
     * @return The CRC32 checksum value as an integer.
     */
    public static int crc32(byte[] data) {
        CRC32 crc32 = new CRC32();
        crc32.update(data);
        return (int) crc32.getValue();
    }

    /**
     * Calculates the CRC32 checksum for the given byte array starting from the specified offset and up to the given length.
     *
     * @param data   The byte array to calculate the CRC32 checksum for.
     * @param offset The starting offset in the byte array.
     * @param len    The length of the data to include in the calculation.
     * @return The CRC32 checksum as an integer value.
     */
    public static int crc32(byte[] data, int offset, int len) {
        CRC32 crc32 = new CRC32();
        crc32.update(data, offset, len);
        return (int) crc32.getValue();
    }


    public static String getTimeFormat(long time){
        return COMPLETE_DATE_FORMAT.format(new Date(time));
    }
}
